package manager;

import service.FileDao;
import service.FileMeta;
import sun.reflect.annotation.ExceptionProxy;

import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: DELL
 * Date: 2023-07-01
 * Time: 9:02
 */

// 文件管理器 -- 针对目录进行扫描并同步结果到数据库中
public class FileManager {

    private FileDao fileDao = new FileDao();

    // 初始化设置选手数目为 1, 当线程池执行完所有任务之后, 就立即调用一次 countDown 撞线.
    private CountDownLatch countDownLatch = null;

    // 用来衡量任务结束的计数器.
    private AtomicInteger taskCount = new AtomicInteger(0);

    /**
     * 通过该方法针对 basePath 描述的目录进行扫描.
     * 把其中的文件和子目录都扫描进去, 并存储到数据库中
     *
     * @param basePath
     */
    public void scanAll(File basePath) {

        System.out.println("[File] scanAll 开始 : ");
        long startTime = System.currentTimeMillis();

        // 保证每次调用 scanAll 都重新创建 CountDownLatch. 重新进行计数了.
        countDownLatch = new CountDownLatch(1);

        // 调用单线程扫描
        //scanAllByThread(basePath);

        // TODO :
        //  为什么使用多线程时, 首次扫描并没有多大效率提升 ? 原因在于其中涉及大量的 insert 操作和 select,
        //  首次扫描是插入操作, 后续扫描是查询操作, 因此后续扫描会比首次扫描快得多
        //  SQLite 插入操作会直接锁住整个表, 如果多个线程尝试并发插入, 第一个线程会先对表进行加锁操作, 完成后再解锁, 解锁后才能让下一个线程进行插入
        //  而如果只是 select 操作, SQLite 允许多个线程并发查询, select 之间没有锁竞争


        scanAllByThreadPool(basePath);

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("[File] scanAll 结束 : " + (endTime - startTime) + "ms");

    }



    /**
     * 实现单线程扫描
     */
    public void scanAllByThread(File basePath) {

        // 判断 basePath 不是目录不进行遍历
        if (!basePath.isDirectory()) {
            return;
        }

        // 调用方法对当前指定路径的目录进行扫描
        scan(basePath);

        // 当前目录中存在子目录, 还需要对这个子目录也进行扫描
        // 列出当前目录下的所有文件/目录
        File[] files = basePath.listFiles();
        if (files == null || files.length == 0) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                // 是目录继续遍历
                scanAllByThread(file);
            }
        }
    }

    private static ExecutorService executorService = Executors.newFixedThreadPool(8);

    private void scanAllByThreadPool(File basePath) {
        // 不是目录则停止遍历, 直接添加即可.
        if (!basePath.isDirectory()) {
            return;
        }

        // 计数器自增
        taskCount.getAndIncrement(); // taskCount++

        // 扫描操作, 放到线程池里完成.
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    scan(basePath);
                } finally {
                    // 计数器自减
                    // 把这个自减逻辑放到 finally 中, 确保自减操作是肯定能执行到的.
                    taskCount.getAndDecrement(); // taskCount--
                    if (taskCount.get() == 0) {
                        // 如果计数器为 0 了, 就通知主线程停表了.
                        countDownLatch.countDown();
                    }
                }
            }
        });

        // 继续递归其他目录.
        File[] files = basePath.listFiles();
        // 当前这个文件不存在, 或者这个文件下是空的没有在继续遍历的必要了
        if (files == null || files.length == 0) {
            return;
        }
        // 递归遍历子目录所有文件
        for (File f : files) {
            if (f.isDirectory()) {
                scanAllByThreadPool(f);
            }
        }
    }


    /**
     * 针对一个目录进行扫描. 只针对 path 目录下的文件进行扫描
     * 把 path 目录下的文件和子目录都扫描出来, 并且保存到数据库中
     * 如果 path 中有子目录, 那么我们只是添加这个子目录的路径, 并不会去进一步扫描这个子目录里的其他东西
     * 如果需要再进一步扫描这个子目录里的其他东西, 则需要把子目录的 path 传进来
     *
     * @param path 这是一个名为 path 的文件对象, 并非真实的路径, 需要通过方法来获取
     */
    public void scan(File path) {
        // 打印扫描的绝对路径, 观察执行过程
        System.out.println("[File] 扫描路径为 : " + path.getAbsolutePath());

        // 1. 列出系统上的真实文件/目录. 只列出来指定路径下的所有子文件和子目录, 并不会列出子目录下的其他文件
        List<FileMeta> scanned = new ArrayList<>();
        File[] files = path.listFiles(); // 列出系统上真实的文件目录
        if (files != null) {
            // 不是空目录
            for (File file : files) {
                // 添加这个真实文件
                scanned.add(new FileMeta(file));
            }
        }

        // TODO : contains 底层调用的是 equals 方法进行比较, 默认比较的是两个对象的地址( 比较对象的身份是否相同 )
        // 这就会有一个问题, scanned 和 saved 是两个地方 new 的不同对象, 肯定地址不相同, 因此比较一定是错误的, 需要重写比较规则

        // 2. 列举出数据库中当前指定目录的内容
        List<FileMeta> saved = fileDao.searchByPath(path.getPath());

        // 3. 找出指定路径下的 scanned 中没有的, 而 saved 数据库中有的进行删除
        List<FileMeta> fileDelete = new ArrayList<>(); // 存储找到的没有的数据库中有而本地上没有的文件

        // 遍历数据库文件看本地文件是否包含数据库文件, 不包含则删除数据库中这些不包含的文件
        for (FileMeta fileMeta : saved) {
            // 本地系统文件不包含数据库中的那些文件
            if (!scanned.contains(fileMeta)) {
                // 进行存储到要删除的集合中最后进行批量删除
                fileDelete.add(fileMeta);
            }
        }

        fileDao.delete(fileDelete);

        // 4. 找出系统中有而数据库中没有的进行添加
        List<FileMeta> forAdd = new ArrayList<>();

        // 遍历系统文件看数据库中是否包含系统文件, 不包含则加入, 最后将这些不包含的文件添加到数据库
        for (FileMeta fileMeta : scanned) {
            if (!saved.contains(fileMeta)) {
                forAdd.add(fileMeta);
            }
        }

        fileDao.add(forAdd);
    }


}
